Skip to content

Add EnterpriseOps-Gym benchmark: resources server, benchmark registration, and per-turn telemetry agent - #2142

Open
mcuevas-nvidia wants to merge 25 commits into
mainfrom
mcuevas-nvidia-bench-integration-enterpriseops-signed
Open

Add EnterpriseOps-Gym benchmark: resources server, benchmark registration, and per-turn telemetry agent#2142
mcuevas-nvidia wants to merge 25 commits into
mainfrom
mcuevas-nvidia-bench-integration-enterpriseops-signed

Conversation

@mcuevas-nvidia

Copy link
Copy Markdown

Summary

This PR integrates ServiceNow's EnterpriseOps-Gym (EOG) into NeMo Gym: a 649-task benchmark of stateful, multi-step enterprise tool use across 8 domains (calendar, CSM, drive, email, HR, ITSM, teams, and cross-domain hybrid), where an agent operates 512 tools against live MCP servers backed by SQL databases and is scored on final database state. The integration is eval-complete and RL-ready (token-ID capture, fractional reward mode, and a validated GRPO rollout-collection recipe), with no changes to NeMo Gym core.

What's included (46 files)

Component Path What it does
Resources server resources_servers/enterpriseops_gym/ Seeds a per-session database on the external EOG MCP containers, proxies tool calls (catch-all /{tool_name} route, pooled aiohttp, per-session x-database-id), runs verifiers concurrently, cleans up idempotently (TTL janitor + delete-on-verify). Supports replica pools (gym_url_pools), per-domain metrics, per-tool latency capture, and a strict_verifiers fractional-reward mode for RL.
Verifier engine .../verifier_engine.py Line-for-line port of EOG's scoring semantics, pinned by golden fixtures generated from the original implementation (33 cases, byte-identical).
Task converter .../convert_tasks.py + benchmarks/enterpriseops/ Converts the public HF dataset (ServiceNow-AI/EnterpriseOps-Gym) to Responses-API task rows at prepare.py time; benchmark registered with prompt_config: null (pre-baked rows). Dataset files are not committed.
Per-turn telemetry agent responses_api_agents/turn_logging_agent/ SimpleAgent subclass with an identical loop that records per-turn timestamps, durations, input/output/cached/reasoning tokens, and tool names, attaching turns to the verify response. Generic — not EOG-specific.
Tests 44 total (42 + 2) Fully offline: a sqlite-backed stub MCP gym (tests/stub_gym.py) plus golden parity fixtures. No containers or network needed to run CI.
Docs PARITY.md, PERF.md, RLPILOT.md Full parity evidence, throughput study, and RL rollout-collection pilot (methodology + measured numbers below).

Verification logic

Each task carries verifier_metadata with a list of verifiers. database_state verifiers
run a SQL query against the session's final database state (via the gym containers'
/api/sql-runner), extract a value, and compare it to the expected value using EOG's
comparison semantics; response_check verifiers score the agent's final message with an
LLM judge (defaults to the policy model, temperature pinned to 0.0, matching EOG). The
public oracle split is 100% database_state (3,496/3,496 verifiers), so scoring there is
fully deterministic given a final DB state. Reward = 1.0 iff all (name-collapsed) verifiers
pass, matching the upstream leaderboard; strict_verifiers: true switches to
every-verifier-counts and a fractional strict_pass_rate for RL shaping.

Scoring fidelity (PARITY.md)

The port is validated bug-for-bug against the upstream harness, preserving its quirks (verifier name-collapse, unknown-gym skips, loose comparison semantics) for leaderboard comparability:

  • Unit level: golden fixtures generated by running the original EOG engine — byte-identical extraction/comparison behavior.
  • Task level: 12/12 identical outcomes and verifier structures on live containers.
  • Full split (649 tasks): port 16.4% vs native 16.8% macro (gpt-4.1-mini, temp 0); per-task agreement 90.4%, disagreements symmetric (McNemar exact p = 0.90).
  • k=5 × both harnesses (6,480 rollouts): Δ −0.23 ± 0.7 pp, per-task preference exactly 67:67.
  • Cross-stack replication (k=5 on dedicated vLLM, Nemotron 3 Nano): formally equivalent within ±2 pp (TOST, α = 0.05).

Performance (PERF.md)

Scale-tested end-to-end at five client concurrencies (c = 8, 16, 32, 64, 128) with a full
649-task pass per level per harness (10 passes, 4×H100 vLLM, identical endpoint): the port
completed 649/649 tasks at every level with zero retries, with success rates flat
across levels. At matched concurrency the port is 1.14–1.71× faster (largest at low
concurrency, nearest the native harness's documented defaults). The gap narrows by design:
both harnesses converge toward the same GPU throughput floor — and reaching it is the key
result. The port saturates the hardware at c=64; the native harness never reaches the
floor in the tested range and needs ~4× the client concurrency for equal throughput.

Net cost: a full-split eval is 1h27m of 4×H100 time (port) vs 4h40m at native's documented
settings — 3.2× GPU-hours. Mechanism: pooled connections, persistent MCP sessions, and
concurrent verifiers keep vLLM's continuous batch fed. (Since the gap is client dead time
relative to GPU service time, it is expected to widen on faster serving hardware, where
saturation demands even more effective concurrency.)

RL readiness (RLPILOT.md)

A config-only pilot (zero code changes) validated GRPO rollout collection end-to-end: 100% token-ID/logprob coverage via return_token_id_information, 15/20 task groups with mixed binary reward at k=8 (mean within-group std 0.341) on a curriculum selected from repeat-run data, plus a measured deployment-sizing guide (sequence-length distribution, memory budget, recommended node shapes).

How to run

# Start the stack (external EOG MCP containers must be running; see resources server README)
ng_run "+config_paths=[resources_servers/enterpriseops_gym/configs/enterpriseops_gym.yaml,responses_api_models/openai_model/configs/openai_model.yaml]" \
  "++enterpriseops_gym.resources_servers.enterpriseops_gym.seed_sql_root=/path/to/EnterpriseOps-Gym"

# Prepare the benchmark data (downloads the public HF dataset) and collect
ng_prepare_benchmark +benchmark_name=enterpriseops
ng_collect_rollouts +agent_name=enterpriseops_gym_simple_agent \
  +input_jsonl_fpath=benchmarks/enterpriseops/data/enterpriseops_oracle_benchmark.jsonl \
  +output_jsonl_fpath=results/enterpriseops.jsonl \
  +responses_create_params.temperature=0.0 +responses_create_params.max_output_tokens=16384

# Tests (offline, no containers needed)
gym env test --resources-server enterpriseops_gym
ng_test +entrypoint=responses_api_agents/turn_logging_agent

Validation on this exact branch state

  • 42/42 + 2/2 tests green after rebase onto current main
  • ruff check + ruff format --check clean; README environment table regenerated via scripts/update_env_list.py
  • Config stack boots via ng_run dry run
  • Live smoke on real MCP containers + gateway model: 5/5 rollouts completed across both agents, all verifiers scored, turn telemetry attached
  • All commits DCO signed-off

Contribution-guide compliance

Mapped to the environment /
benchmark guides:

  • Required files — all present: app.py, configs/*.yaml (valid domain: agent),
    tests/test_app.py (42 tests), data/example.jsonl (5 tasks),
    data/example_rollouts.jsonl (5 pre-generated rollouts against live containers; note
    these samples are CSM-domain, the benchmark's hardest — near-zero rewards on them are
    expected and consistent with the full-split CSM rate of ~4%), requirements.txt,
    README.md with licensing information.
  • Reward profiling — run on a closed model (gpt-4.1-mini: 16.4% macro) and an open
    thinking model (Nemotron 3 Nano, reasoning on: 22–25%), i.e. the instruct+thinking
    mixture the guide asks for. Scores sit inside the official leaderboard's published range
    (Qwen3-4B 13.6% … GPT-5-Mini 22.0%) with a coherent domain pattern (email easiest, CSM
    hardest, matching the leaderboard). This benchmark is legitimately hard — no public model
    reaches 30%.
  • Variance — k=5 repeat runs on both stacks; mean@5 resolves ±0.7 pp (< 1% as required).
    Calibration guidance for users is included in PARITY.md.
  • Failure-case analysis — performed extensively across parity, perf, and RL-pilot runs
    (documented in the three reports).
  • Original-repo reproduction — instead of reproducing a leaderboard model's published
    number (leaderboard models weren't available on our serving), we ran the original EOG
    harness side-by-side
    on identical models, containers, and endpoints: per-task agreement
    with symmetric disagreements (McNemar p = 0.90) and formal TOST equivalence within ±2 pp.
    This isolates harness fidelity even more directly; happy to additionally run a listed
    leaderboard model if reviewers want the published-number check.
  • Stacked PRs — per the development-setup guide, happy to restack this as layers
    (resources server + tests → benchmark registration → turn-logging agent → reports) if
    reviewers prefer; presented as one PR first since the layers are tightly coupled by the
    parity evidence.

Design decisions & notes for reviewers

  • Hand-rolled MCP client instead of the official SDK (rationale in mcp_client.py docstring): the SDK's transport is httpx-based (banned for async here), EOG needs session-level and per-call x-database-id headers, and half the surface is non-MCP REST (/api/seed-database etc.).
  • Upstream EOG quirks intentionally preserved and documented (PARITY.md §1) rather than fixed, to keep leaderboard comparability; strict_verifiers: true opts into every-verifier-counts scoring for RL.
  • turn_logging_agent is separable — it's a general-purpose agent; happy to split it into its own PR if preferred.
  • verified: false per convention for new resources servers.
  • External dependency: the benchmark requires the EOG Docker containers (from the upstream EOG repo) at eval time; unit tests do not.
  • Operational findings that affect large runs (MCP container fd leak ~5/task, upstream runner's silent task drops) are documented in PARITY.md §6; we plan to file these upstream against EOG.

Data provenance & licensing

EnterpriseOps-Gym is Apache 2.0 (code) with a public HF dataset (ServiceNow-AI/EnterpriseOps-Gym). This PR commits only: tool-schema snapshots captured from the public EOG containers (7 JSON files, 512 tools), 13 sample tasks derived from the EOG repo's task files, 5 example rollouts generated against live containers, one synthetic hybrid task hand-authored for tests (written with LLM assistance against live container schemas — disclosed per the synthetic-data guideline), and golden verifier fixtures generated by running the EOG engine. The full benchmark dataset is downloaded at prepare.py time and gitignored.

mcuevas-nvidia and others added 10 commits July 24, 2026 21:43
…er-turn telemetry

Adapts the ServiceNow EnterpriseOps-Gym benchmark (Apache 2.0; 8 enterprise
domains, external MCP gym servers, SQL verifiers over final DB state) to
NeMo Gym:

- resources_servers/enterpriseops_gym: per-rollout DB seeding (SQL content
  cache + per-gym seed semaphores), catch-all /{tool_name} MCP proxy with
  EOG-parity observations and per-tool latency capture, idempotent /verify
  with guaranteed DB deletion, TTL janitor for killed rollouts, replica
  pools (gym_url_pools) for horizontal MCP scale-out, and per-domain
  aggregate metrics (leaderboard-style macro average).
- verifier_engine.py is a line-for-line port of the upstream engine,
  preserving its quirks for score parity (verifier name-collapse where
  duplicate-named verifiers overwrite; loose comparison semantics; skipped
  unknown-gym verifiers), pinned by golden fixtures generated from the
  original implementation. strict_verifiers=true switches the reward to
  every-verifier-counts for RL shaping.
- convert_tasks.py / snapshot_tools.py convert EOG tasks (local or the
  ServiceNow-AI/EnterpriseOps-Gym HF dataset) into NeMo Gym JSONL, baking
  tool schemas from live tools/list snapshots with per-task gym-order
  merge semantics (hybrid parity).
- benchmarks/enterpriseops: oracle public split (649 tasks) with HF
  download and offline local fallbacks.
- responses_api_agents/turn_logging_agent: behaviorally identical
  simple_agent variant that records per-turn telemetry (timestamps,
  input/output/cached tokens, tool names) and attaches it to verify
  responses; export_eval_telemetry.py emits the eval team's 21-field
  per-turn JSONL schema.

Validated at full scale against the native harness on the same 649 tasks,
model, and containers: macro success 16.4-17.8% across three runs, 90-94%
per-task agreement, McNemar p>=0.21 (no detectable harness bias), and
100% identical collapsed-verifier scoring structure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Consolidates the equivalence case vs the native harness: golden-fixture
unit parity, 12/12 live task parity, full-public-split single-run
comparisons (McNemar p>=0.21, 100% scoring-structure agreement), and the
k=5 interleaved variance experiment (6,480 rollouts): mean@5 macro
16.54+/-0.73 vs 16.76+/-0.99 (delta -0.23pp), per-task preference 67:67,
and a direction-free gateway serving-path effect (+3.49pp outcome-flip
excess, permutation p<0.002) attributed to Responses-vs-ChatCompletions
serving rather than either harness. Includes calibration guidance
(report mean@k; ~6.3% of outcomes flip on any rerun) and upstream-relevant
operational findings (container fd leak, silent task drops, resume mode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Upstream #1682 made the MCP Python SDK a core dependency, but for
Gym-as-MCP-server (the inverse of this module's Gym-as-MCP-client role).
Records why the SDK client is not a fit here: httpx transport (banned for
high-concurrency async), session-level vs required per-call isolation
headers, non-MCP REST endpoints comprising half the surface, and frozen
upstream protocol version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
…ed GPU

Full oracle-split sweep (649 tasks x 2 harnesses x c in {8,16,32,64,128})
on 4xH100 TP=4 serving Nemotron 3 Nano FP8 locally, both harnesses on the
identical chat-completions endpoint. The port is 1.14-1.71x faster at
matched concurrency (largest at realistic low-c settings), needs ~4x less
client concurrency for equal throughput, and saturates the hardware at
c=64 while native never reaches the throughput floor in the tested range.
Success rates identical within noise at every level. Mechanism: GPU batch
starvation from native's per-request connections, per-task handshakes,
and sequential verifiers (GPU 98% busy both sides, client CPU idle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Treats the 4xH100 performance sweep as an independent k=5 replication on
dedicated vLLM serving (Nemotron 3 Nano FP8, both harnesses on one
endpoint). Confirms the serving-path attribution via its designed
falsification test (cross-harness trajectory excess 3.49 -> 1.57pp, 55%
-> 11% of the noise floor), establishes formal TOST equivalence within
+/-2pp at alpha=0.05 with direction-free residuals (112:106 task
preference, sign p=0.735), and adds a determinism calibration for
reasoning models (41% flaky tasks vs 20% non-reasoning; report mean@5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Config-only GRPO readiness validation on the 4xH100 stack: token-ID
capture via return_token_id_information, curriculum selection from
repeat-run sweep data, and group-mixing results (15/20 binary-mixed
at k=8, mean group std 0.341). Documents the unshuffled-benchmark
+limit pitfall and operational notes for long collection runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Measured train-sequence lengths (exact from v3 token IDs, band-wide via
calibrated estimate): curriculum yield is 63% at a 32k cap vs 94% at
64k, which drives the shape ranking (B300/B200 single node > H200 >
2x8 H100 disaggregated > single 8x H100 > LoRA fallback). Includes
trainer memory budget, step-time model, node layout, and the
pre-registered proof-point run sketch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Generated by scripts/update_env_list.py; the turnlog overlay config gains
a metadata block (a no-op merge over the inherited server) so the table
generator can render its row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Per the environment contribution guide: data/example_rollouts.jsonl
(5 pre-generated rollouts from example.jsonl against live containers)
and a licensing/data-provenance section in the server README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

mcuevas-nvidia and others added 2 commits July 26, 2026 22:13
Generated via gym dataset collate +mode=example_validation; CI's
should_validate_data gate requires it alongside example.jsonl and
example_rollouts.jsonl.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
…h-integration-enterpriseops

Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@github-actions github-actions Bot added the sla:triage-overdue Review assignment is over the one-business-day SLA label Jul 27, 2026
The five flagged strings in enterpriseops_gym/data/tools/drive.json are
example Google Drive document IDs from the upstream EOG container's tool
schemas (one is the sample spreadsheet ID from Google's own API docs),
not credentials. Baseline updated with detect-secrets 1.5.0 to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@mcuevas-nvidia
mcuevas-nvidia requested a review from a team as a code owner July 27, 2026 01:29
gym env test discovery only treats modules with a README.md as
testable; with fail_on_total_and_test_mismatch=true the missing README
failed CI's shared server-tests job (found 137 modules, tested 136).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@@ -0,0 +1,3436 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can these long files be pulled from somewhere like huggingface instead of committed?

@cmunley1

Copy link
Copy Markdown
Contributor

/claude review

@github-actions github-actions Bot removed the sla:triage-overdue Review assignment is over the one-business-day SLA label Jul 27, 2026
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

SHIP WITH CARE — no BLOCKERs. The verifier port and reward aggregation are the highest-risk surface here and they're the most carefully done part of the PR: EOG parity quirks (name-collapse, unknown-gym skips, loose comparisons) are documented and pinned by golden fixtures, strict_verifiers cleanly separates leaderboard-parity reward from RL reward, verify is idempotent and deletes DBs in a finally, the MCP client correctly routes all async HTTP through nemo_gym.server_utils.request() (no httpx, no ray.get()), and per-call x-database-id isolation is the right call over the SDK's session-level headers. No core changes, tests are fully offline, deps declared. Good work.

Two non-blocking findings, both inline:

  1. RISK (turn_logging_agent/app.py): the agent hardcodes bare /v1/responses where simple_agent uses url_path_for_request/url_path_for_run, dropping the /ng-rollout/<id> capture-correlation prefix. No-op when observability is off, so eval/reward and inline return_token_id_information are unaffected — but this agent's whole purpose is RL telemetry, which is where capture gets enabled. The docstring even claims step-for-step parity; this is the divergence.

  2. RISK (app.py verify): all([]) is True, so a fully empty or all-skipped verifier set scores reward 1.0. A gym_name/pool misconfig silently converts every task to a false pass — inflates eval macro-rates and feeds spurious positive reward into GRPO. Guard the strict path (bool(strict_passed) and all(...)) and at least log.warning on num_verifiers_scored == 0.

Neither blocks merge; #2 is the one I'd want resolved before this feeds a training run.

Comment thread responses_api_agents/turn_logging_agent/app.py
Comment thread resources_servers/enterpriseops_gym/app.py
mcuevas-nvidia and others added 2 commits July 28, 2026 18:13
Mirror simple_agent's url_path_for_request/url_path_for_run so
/ng-rollout/<id>-prefixed self-calls keep per-rollout observability
correlation on downstream model calls. Adds a regression test driving
the prefixed route. Addresses PR #2142 review feedback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
all([]) is True, so a task with zero scorable verifiers awarded
strict reward 1.0. Guard the strict path (which feeds RL rewards);
the collapsed parity path intentionally keeps upstream's all([])
semantics for leaderboard comparability, now documented in place.
Addresses PR #2142 review feedback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@mcuevas-nvidia

mcuevas-nvidia commented Jul 28, 2026 via email

Copy link
Copy Markdown
Author

…h-integration-enterpriseops

Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@ritaneves
ritaneves requested a review from Glorf July 30, 2026 08:24
@github-actions github-actions Bot added the sla:review-overdue Review response is over the one-business-day SLA label Jul 31, 2026
@ritaneves
ritaneves removed the request for review from Glorf August 7, 2026 10:44
@github-actions github-actions Bot removed the sla:review-overdue Review response is over the one-business-day SLA label Aug 7, 2026
@ritaneves
ritaneves requested a review from bxyu-nvidia August 8, 2026 08:52
@github-actions github-actions Bot added the sla:review-overdue Review response is over the one-business-day SLA label Aug 10, 2026
@cmunley1

Copy link
Copy Markdown
Contributor

Could we do option 1 @mcuevas-nvidia ?

@cmunley1

Copy link
Copy Markdown
Contributor

/claude review

}
],
"results": {
"resources_servers/enterpriseops_gym/data/tools/drive.json": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you use inline # pragma: allowlist secret instead of global config?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might wanna remove images for keeping repo slim as we add many envs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its fine if we leave this, its not too big.

@@ -0,0 +1,34 @@
# EnterpriseOps-Gym with the per-turn-telemetry agent (eval-team logging schema).
# Adds a turn_logging_agent alongside the standard stack; collect with
# ng_collect_rollouts +agent_name=enterpriseops_gym_turn_logging_agent ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could u update this to new cli

--output resources_servers/enterpriseops_gym/data/csm_revised.jsonl

# Run servers + collect rollouts
ng_run "+config_paths=[resources_servers/enterpriseops_gym/configs/enterpriseops_gym.yaml,responses_api_models/openai_model/configs/openai_model.yaml]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new cli also would be good

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we should update to the latest commands

# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""SimpleAgent with per-turn telemetry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment: I wonder if this should go directly into simple agent or we see if the model call capture recent work is sufficient. I know there are some other efforts on observability.

# `num_verifiers_scored` exposes the condition to callers.
total_verifiers = len(verification_results)
passed_verifiers = sum(1 for v in verification_results.values() if v.get("passed", False))
overall_success = all(v["passed"] for v in verification_results.values())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE — silent score inflation on config error (default parity mode).

WHAT: With strict_verifiers: false (the default, per enterpriseops_gym.yaml), if every verifier is skipped for an unknown gym_name, verification_results is empty → overall_success = all([]) == Truereward = 1.0, while verifier_pass_rate is 0.0.

BLAST RADIUS: A fat-fingered gym_servers_config.mcp_server_name (so every verifier's gym_name misses) makes every rollout in that eval score a false pass, inflating the report. You've correctly documented the deliberate empty-set parity quirk (PARITY.md §1) and strict mode fixes it — the concern is specifically the config-typo path, which looks identical to a legitimate all-pass.

FIX (optional, defense-in-depth): the data is already there — num_verifiers_scored/num_verifiers_defined are surfaced. Consider a startup/seed-time warn (or an eval-side check) when num_verifiers_scored == 0 for a nonzero num_verifiers_defined, so a mass-skip surfaces instead of reading as 100%. Author's call — not a merge blocker.

@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

SHIP — no blocking reliability concerns.

Reviewed the full change (50 files): the enterpriseops_gym resources server, the turn_logging_agent, the converter/telemetry/snapshot tooling, and the benchmark wiring. This is a careful, heavily-documented port of ServiceNow's EnterpriseOps-Gym.

What I checked hardest, and what held up:

  • Async / HTTP hygiene. mcp_client.py and EnterpriseOpsGymResourcesServer._judge route all outbound calls through the global aiohttp client (nemo_gym.server_utils.request() / ServerClient). No httpx.AsyncClient, no ray.get(), no dropped await. The port explicitly swaps upstream's per-request httpx client for the pooled client and moves per-rollout state (database_id/context) to per-call headers instead of mutating the client — the racy pattern is gone. Seeding is bounded by per-gym-server asyncio.Semaphore; verifiers fan out via asyncio.gather.
  • Verifier/scorer correctness. Extraction/comparison/collapse semantics are pinned to the upstream engine via goldens generated by generate_parity_golden.py (which imports benchmark.verifier), so the parity tests aren't circular. The strict RL path correctly rejects the all([]) == True empty-set trap that the parity path deliberately keeps. build_model_response, name-collapse, and unknown-gym skip are all covered by offline stub-gym end-to-end tests.
  • Public API. Additive only — no changes to BaseServer/SimpleServer/SimpleResponsesAPIAgent. compute_metrics override matches the AggregateMetricsMixin contract.
  • Config/deps. Defaults live in the exemplar YAML; datasets is already declared in pyproject.toml; no undeclared imports. verified: false is correctly set.
  • turn_logging_agent mirrors simple_agent's loop step-for-step (cookie threading, malformed-arg handling, termination) and correctly restores the cached/reasoning token detail that simple_agent zeroes.

One inline NOTE (defense-in-depth, author's call): in default parity mode, a gym_servers_config name typo that skips every verifier scores reward = 1.0 and reads identically to a legitimate all-pass — worth a warn when num_verifiers_scored == 0 for a nonzero defined count.

mcuevas-nvidia and others added 8 commits August 28, 2026 18:45
Resolves the README environment table by regenerating it with
scripts/update_env_list.py, and takes main's .secrets.baseline (the
enterpriseops entries are removed in a follow-up commit that deletes
the file they reference).

Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Replaces the deprecated ng_* aliases with their `gym` equivalents per
fern/versions/latest/pages/reference/cli-commands.mdx:

  ng_run              -> gym env start
  ng_collect_rollouts -> gym eval run --no-serve
  ng_prepare_benchmark-> gym eval prepare

and converts the Hydra +key=value overrides to the corresponding flags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
…ing them

The seven per-domain tools/list snapshots are 30,724 lines of generated JSON
-- 86% of this PR's added lines -- and are build-time inputs only: convert_tasks
bakes them into dataset rows at prepare time and nothing reads them at run time.
snapshot_tools.py can re-capture them from the upstream containers at any time.

Follows the conversational_tool_use_simulation precedent: a new prepare.py
fetches nvidia/NeMo-Gym-EnterpriseOps-Assets at a pinned revision, validates it
against a (file_count, tree_sha256) pin, and materializes data/tools/, which is
now gitignored. ensure_tool_snapshots() is idempotent and runs before the
benchmark opens its output file, so a download failure cannot truncate an
existing benchmark JSONL. NEMO_GYM_EOG_TOOLS_DIR skips the download for
air-gapped machines, validated against the same pin.

Reverts the .secrets.baseline entries, which only existed for drive.json --
that closes the inline-pragma request too, since there is nothing left to
allowlist.

Also drops csm_revised.jsonl and itsm_revised.jsonl (405 KB): both are output
of the documented convert_tasks command, and neither is referenced by any code,
config, or test.

DEFAULT_REVISION is a placeholder until the dataset repo is published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
With strict_verifiers false (the default), a gym_servers_config typo means no
verifier matches a live gym, verification_results is empty, and the parity path
scores all([]) as a pass. That is deliberate -- it is upstream EOG behavior and
PARITY.md pins it -- but in aggregate metrics it is indistinguishable from a
genuine 100% pass, so a mass skip can silently inflate a whole eval.

Log a warning naming the referenced gym_name(s) and the session's actual gyms
when verifiers were defined but none were scored. Scoring is unchanged; this is
observability only. The warning fires only on a total skip, not a partial one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
nvidia/NeMo-Gym-EnterpriseOps-Assets is published; replace the placeholder
with its head SHA. Verified with the committed defaults against a cold HF
cache: download, filename and checksum validation, and materialization all
succeed, and the six committed rows still reproduce byte-identical tool
schemas from the hosted snapshots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
The dataset is now public and ungated. Re-pin to the revision carrying the
corrected NOTICE, which describes provenance rather than asserting upstream
copyright ownership (upstream ships no NOTICE file, so there are no attribution
notices to propagate under Apache-2.0 section 4(d)).

Verified anonymously with no token and a cold cache: download, validation, and
materialization all succeed, so external contributors and CI can prepare.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
Running the full 649-task oracle conversion surfaces two tasks whose
selected_tools do not resolve against the container tool surface. Record the
cause, the evidence that it is not snapshot staleness, and the bound on the
headline metric.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
#2827 (2026-08-28) made every resources server ship a task_data.py describing
its dataset rows; it landed after this PR branched, so the merge of main left
two repo tests failing.

Rows nest everything in an untyped verifier_metadata bucket, so the schema is
flat with legacy_location annotations. Every field is Optional because the
server reads the bucket only via .get(...) or <default> and never 422s on its
contents. Shapes derived from all 655 committed and generated rows: 744
gym_servers_config entries and 3,532 verifiers, with user_info the one variable
field (absent from 44 entries, str or dict where present).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Marc Cuevas <mcuevas@nvidia.com>
@mcuevas-nvidia

Copy link
Copy Markdown
Author

Addressed all PR comments. Ready for re-review.

The seven data/tools/*.json snapshots now live in a HuggingFace assets dataset and are
fetched at prepare time. That drops 30,724 lines from the diff (86% of what this PR added);
the PR goes from 35,571 added lines to 5,568.

Approach follows the conversational_tool_use_simulation precedent: a new
resources_servers/enterpriseops_gym/prepare.py snapshot-downloads a pinned revision,
validates the filename set and a tree_sha256 over the flat directory, then materializes
data/tools/ (now gitignored).

prepare.py --print-hash reproduces the pin:

file_count  = 7
tree_sha256 = d9ee1a279ec85985ba3fc59f2f9502a9c470301062800cab4d07c87b123354b3

ensure_tool_snapshots() is idempotent (a directory already matching the pin makes no
network call, and a stray file in the gitignored dir doesn't force a re-download), and it
runs before the benchmark opens its output file, so a download failure can't truncate an
existing benchmark JSONL.

A fully air-gapped machine now needs NEMO_GYM_EOG_TOOLS_DIR pointing at a directory fetched elsewhere,
validated against the same checksum. There's an hf download one-liner in the README.

Also in this push:

  • Deleted csm_revised.jsonl and itsm_revised.jsonl (405 KB). Both are the output of the
    convert_tasks.py command the README documents, and neither is referenced by any code,
    config, or test — csm_revised.jsonl appeared only in a README example, as the
    --output of the convert command and the input of the rollout command. Better regenerated
    than stored. hybrid_synthetic.jsonl (10 KB) stays: it's hand-authored and not derivable.
    Its README description was also wrong about being used by tests; fixed.
  • Reverted .secrets.baseline to zero diff. All five entries came from drive.json, so
    deleting it leaves nothing to allowlist — that closes the inline-pragma request without
    needing pragmas at all.
  • Modernized the CLI in a separate commit (ng_rungym env start,
    ng_collect_rolloutsgym eval run --no-serve, ng_prepare_benchmark
    gym eval prepare, Hydra overrides → flags) across the server README, the benchmark
    README, enterpriseops_gym_turnlog.yaml, PARITY.md, and RLPILOT.md.
  • Took the num_verifiers_scored == 0 NOTE from the bot reviews. Scoring is unchanged —
    the parity path still keeps upstream's all([]) == True, per PARITY.md §1 — but a total
    skip now logs a warning naming the referenced gym_name(s) and the session's actual gyms,
    so a gym_servers_config typo stops reading as a legitimate 100% pass. Fires only on a
    total skip, not a partial one; both cases covered by tests.
  • On turn_logging_agent vs simple_agent, the default path is simple_agent; enterpriseops_gym.yaml registers enterpriseops_gym_simple_agent, and the benchmark's enterpriseops_benchmark_simple_agent inherits from it. turn_logging_agent is additive and opt-in via a separate enterpriseops_gym_turnlog.yaml; nothing gets it unless a config asks for it by name. simple_agent has emitted native per-turn TrajectoryTurn records since [fix] Standardize rollout trajectory records and document producer coverage #2293, which lands most of what the fork was written for in July. Keep it in-tree for now while validating the eval and RL paths against simple_agent end to end, then remove it in a follow-up rather than widen this PR.

Verified: the download → checksum → materialize path end-to-end against the real Hub
(fresh fetch, no-op re-run, tamper repair, wrong-revision rejection); 12 new offline tests
plus the existing converter tests; ruff clean; the 6 committed rows still reproduce
byte-identical tool schemas from the hosted snapshots.

Status: the dataset is published and public at
[nvidia/NeMo-Gym-EnterpriseOps-Assets](https://huggingface.co/datasets/nvidia/NeMo-Gym-EnterpriseOps-Assets),
pinned at 8918dc64b8575d5ff476e62e1cc3687523ab59c2. Verified anonymously with no token and a
cold cache, so external contributors and CI can run prepare.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sla:review-overdue Review response is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants